Skip to content

25 contextlib上下文管理器

with语句你肯定用过——打开文件、获取锁、连接数据库都用它。它能保证资源被正确释放,即使发生异常也不怕。contextlib模块让你更轻松地创建自己的上下文管理器。

一、@contextmanager装饰器

1.1 基本用法

不用写__enter____exit__方法,用生成器就能创建上下文管理器。

python
from contextlib import contextmanager

@contextmanager
def managed_resource(name):
    print(f"获取资源 {name}")
    try:
        yield name  # 产出值给with语句
    finally:
        print(f"释放资源 {name}")

# 使用
with managed_resource("数据库") as resource:
    print(f"使用 {resource}")

输出:

获取资源 数据库
使用 数据库
释放资源 数据库

1.2 带异常处理

python
from contextlib import contextmanager

@contextmanager
def error_handler():
    try:
        yield
    except ValueError as e:
        print(f"捕获到错误: {e}")
    except Exception as e:
        print(f"其他错误: {e}")
        raise

with error_handler():
    raise ValueError("测试错误")

# 输出: 捕获到错误: 测试错误

1.3 返回值

python
from contextlib import contextmanager

@contextmanager
def open_file(path, mode):
    f = open(path, mode)
    try:
        yield f  # 产出文件对象
    finally:
        f.close()

with open_file("test.txt", "w") as f:
    f.write("Hello")

二、实用示例

2.1 计时器

python
from contextlib import contextmanager
import time

@contextmanager
def timer():
    start = time.perf_counter()
    yield
    elapsed = time.perf_counter() - start
    print(f"耗时: {elapsed:.4f}秒")

with timer():
    sum(range(1000000))
# 输出: 耗时: 0.0312秒

2.2 临时修改目录

python
from contextlib import contextmanager
import os

@contextmanager
def change_dir(path):
    old_dir = os.getcwd()
    os.chdir(path)
    try:
        yield
    finally:
        os.chdir(old_dir)

with change_dir("/tmp"):
    print(os.getcwd())  # /tmp
print(os.getcwd())  # 原来的目录

2.3 临时修改环境变量

python
from contextlib import contextmanager
import os

@contextmanager
def env_var(key, value):
    old_value = os.environ.get(key)
    os.environ[key] = value
    try:
        yield
    finally:
        if old_value is None:
            del os.environ[key]
        else:
            os.environ[key] = old_value

with env_var("DEBUG", "1"):
    print(os.environ["DEBUG"])  # 1
print(os.environ.get("DEBUG"))  # None

2.4 数据库事务

python
from contextlib import contextmanager

@contextmanager
def transaction(connection):
    try:
        yield connection
        connection.commit()
        print("事务提交")
    except Exception:
        connection.rollback()
        print("事务回滚")
        raise

# 使用
with transaction(conn):
    conn.execute("INSERT INTO users ...")

三、suppress():抑制异常

python
from contextlib import suppress

# 文件不存在时不报错
with suppress(FileNotFoundError):
    os.remove("nonexistent.txt")

# 等价于
try:
    os.remove("nonexistent.txt")
except FileNotFoundError:
    pass

多个异常:

python
with suppress(FileNotFoundError, PermissionError):
    os.remove("somefile.txt")

四、redirect_stdout / redirect_stderr

4.1 重定向输出

python
from contextlib import redirect_stdout
import io

# 捕获print输出
f = io.StringIO()
with redirect_stdout(f):
    print("这行被重定向了")
    print("这行也是")

output = f.getvalue()
print(f"捕获到: {output}")

4.2 重定向到文件

python
from contextlib import redirect_stdout

with open("output.txt", "w") as f:
    with redirect_stdout(f):
        print("写入文件")
        print("而不是终端")

4.3 同时重定向stdout和stderr

python
from contextlib import redirect_stdout, redirect_stderr
import io

stdout_capture = io.StringIO()
stderr_capture = io.StringIO()

with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture):
    print("stdout内容")
    import sys
    print("stderr内容", file=sys.stderr)

五、closing():自动关闭

python
from contextlib import closing

# 确保对象的close()方法被调用
with closing(open("file.txt")) as f:
    content = f.read()

# 等价于
with open("file.txt") as f:
    content = f.read()

适用于没有实现上下文管理器协议但有close()方法的对象。

六、nullcontext():空操作

python
from contextlib import nullcontext

# 有时候不需要上下文管理,但代码结构需要
def process(use_resource=True):
    cm = managed_resource("test") if use_resource else nullcontext()
    with cm:
        print("处理中")

process(True)   # 使用资源
process(False)  # 不使用资源

七、ExitStack:动态管理多个上下文

7.1 基本用法

python
from contextlib import ExitStack

with ExitStack() as stack:
    # 动态添加上下文管理器
    files = [
        stack.enter_context(open(f"file{i}.txt", "w"))
        for i in range(3)
    ]

    for f in files:
        f.write("hello")

# 所有文件在退出时自动关闭

7.2 动态决定上下文

python
from contextlib import ExitStack, suppress

with ExitStack() as stack:
    # 根据条件添加不同的上下文
    if debug_mode:
        stack.enter_context(redirect_stdout(log_file))

    stack.enter_context(suppress(FileNotFoundError))

    # 正常逻辑
    do_something()

7.3 注册清理回调

python
from contextlib import ExitStack

def cleanup():
    print("清理资源")

with ExitStack() as stack:
    stack.callback(cleanup)  # 退出时调用
    print("正常逻辑")
# 输出:
# 正常逻辑
# 清理资源

八、asynccontextmanager:异步版本

python
from contextlib import asynccontextmanager
import asyncio

@asynccontextmanager
async def async_resource():
    print("获取异步资源")
    try:
        yield
    finally:
        print("释放异步资源")

async def main():
    async with async_resource():
        print("使用异步资源")

asyncio.run(main())

九、实战场景

9.1 日志上下文

python
from contextlib import contextmanager
import logging

@contextmanager
def log_context(logger, level, message):
    logger.log(level, f"开始: {message}")
    try:
        yield
        logger.log(level, f"完成: {message}")
    except Exception as e:
        logger.error(f"失败: {message} - {e}")
        raise

logger = logging.getLogger(__name__)
with log_context(logger, logging.INFO, "数据处理"):
    process_data()

9.2 临时配置

python
from contextlib import contextmanager

@contextmanager
def temp_config(config, **overrides):
    original = {k: config[k] for k in overrides}
    config.update(overrides)
    try:
        yield config
    finally:
        config.update(original)

config = {"debug": False, "verbose": False}
with temp_config(config, debug=True, verbose=True):
    print(config)  # {'debug': True, 'verbose': True}
print(config)  # {'debug': False, 'verbose': False}

9.3 重试机制

python
from contextlib import contextmanager

@contextmanager
def retry(max_attempts=3):
    for attempt in range(max_attempts):
        try:
            yield attempt
            break
        except Exception as e:
            if attempt == max_attempts - 1:
                raise
            print(f"重试 {attempt + 1}/{max_attempts}")

with retry(3) as attempt:
    print(f"尝试 {attempt}")
    # 可能会抛异常的操作

十、总结

contextlib的核心:

组件用途
@contextmanager用生成器创建上下文管理器
@asynccontextmanager异步版本
suppress()抑制异常
redirect_stdout/stderr重定向输出
closing()自动调用close()
nullcontext()空操作
ExitStack动态管理多个上下文

使用场景:

  • 资源管理(文件、数据库连接、网络连接)
  • 临时修改状态(环境变量、工作目录、配置)
  • 异常处理和清理
  • 日志和计时

@contextmanager是最常用的,记住它的用法就够了:yield之前是__enter__yield之后是__exit__